/*
 * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
 * The software in this package is published under the terms of the CPAL v1.0
 * license, a copy of which has been included with this distribution in the
 * LICENSE.txt file.
 */
package org.mule.runtime.core.util;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * This is the class file reader for obtaining the parameter names for declared methods in a class. The class must have debugging
 * attributes for us to obtain this information.
 * <p>
 * This does not work for inherited methods. To obtain parameter names for inherited methods, you must use a paramReader for the
 * class that originally declared the method.
 * <p>
 * don't get tricky, it's the bare minimum. Instances of this class are not threadsafe -- don't share them.
 * <p>
 * 
 * @author Edwin Smith, Macromedia
 */
public class ClassReader extends ByteArrayInputStream {

  // constants values that appear in java class files,
  // from jvm spec 2nd ed, section 4.4, pp 103
  private static final int CONSTANT_CLASS = 7;
  private static final int CONSTANT_FIELDREF = 9;
  private static final int CONSTANT_METHODREF = 10;
  private static final int CONSTANT_INTERFACE_METHOD_REF = 11;
  private static final int CONSTANT_STRING = 8;
  private static final int CONSTANT_INTEGER = 3;
  private static final int CONSTANT_FLOAT = 4;
  private static final int CONSTANT_LONG = 5;
  private static final int CONSTANT_DOUBLE = 6;
  private static final int CONSTANT_NAME_AND_TYPE = 12;
  private static final int CONSTANT_UTF_8 = 1;
  /**
   * the constant pool. constant pool indices in the class file directly index into this array. The value stored in this array is
   * the position in the class file where that constant begins.
   */
  private int[] cpoolIndex;
  private Object[] cpool;

  private Map<String, Method> attrMethods;

  protected ClassReader(byte buf[], Map<String, Method> attrMethods) {
    super(buf);

    this.attrMethods = attrMethods;
  }

  /**
   * load the bytecode for a given class, by using the class's defining classloader and assuming that for a class named P.C, the
   * bytecodes are in a resource named /P/C.class.
   * 
   * @param c the class of interest
   * @return a byte array containing the bytecode
   * @throws IOException
   */
  protected static byte[] getBytes(Class c) throws IOException {
    InputStream fin = c.getResourceAsStream('/' + c.getName().replace('.', '/') + ".class");
    if (fin == null) {
      throw new IOException();
    }
    try {
      ByteArrayOutputStream out = new ByteArrayOutputStream();
      byte[] buf = new byte[1024];
      int actual;
      do {
        actual = fin.read(buf);
        if (actual > 0) {
          out.write(buf, 0, actual);
        }
      } while (actual > 0);
      return out.toByteArray();
    } finally {
      fin.close();
    }
  }

  static String classDescriptorToName(String desc) {
    return desc.replace('/', '.');
  }

  protected static Map<String, Method> findAttributeReaders(Class c) {
    Map<String, Method> map = new HashMap<String, Method>();
    Method[] methods = c.getMethods();

    for (int i = 0; i < methods.length; i++) {
      String name = methods[i].getName();
      if (name.startsWith("read") && methods[i].getReturnType() == void.class) {
        map.put(name.substring(4), methods[i]);
      }
    }

    return map;
  }

  protected static String getSignature(Member method, Class[] paramTypes) {
    // compute the method descriptor

    StringBuilder b = new StringBuilder((method instanceof Method) ? method.getName() : "<init>");
    b.append('(');

    for (int i = 0; i < paramTypes.length; i++) {
      addDescriptor(b, paramTypes[i]);
    }

    b.append(')');
    if (method instanceof Method) {
      addDescriptor(b, ((Method) method).getReturnType());
    } else if (method instanceof Constructor) {
      addDescriptor(b, void.class);
    }

    return b.toString();
  }

  private static void addDescriptor(StringBuilder b, Class c) {
    if (c.isPrimitive()) {
      if (c == void.class) {
        b.append('V');
      } else if (c == int.class) {
        b.append('I');
      } else if (c == boolean.class) {
        b.append('Z');
      } else if (c == byte.class) {
        b.append('B');
      } else if (c == short.class) {
        b.append('S');
      } else if (c == long.class) {
        b.append('J');
      } else if (c == char.class) {
        b.append('C');
      } else if (c == float.class) {
        b.append('F');
      } else if (c == double.class) {
        b.append('D');
      }
    } else if (c.isArray()) {
      b.append('[');
      addDescriptor(b, c.getComponentType());
    } else {
      b.append('L').append(c.getName().replace('.', '/')).append(';');
    }
  }

  /**
   * @return the next unsigned 16 bit value
   */
  protected final int readShort() {
    return (read() << 8) | read();
  }

  /**
   * @return the next signed 32 bit value
   */
  protected final int readInt() {
    return (read() << 24) | (read() << 16) | (read() << 8) | read();
  }

  /**
   * skip n bytes in the input stream.
   */
  protected void skipFully(int n) throws IOException {
    while (n > 0) {
      int c = (int) skip(n);
      if (c <= 0) {
        throw new EOFException();
      }

      n -= c;
    }
  }

  protected final Member resolveMethod(int index) throws IOException, ClassNotFoundException, NoSuchMethodException {
    int oldPos = pos;
    try {
      Member m = (Member) cpool[index];
      if (m == null) {
        pos = cpoolIndex[index];
        Class owner = resolveClass(readShort());
        NameAndType nt = resolveNameAndType(readShort());
        String signature = nt.name + nt.type;
        if ("<init>".equals(nt.name)) {
          Constructor[] ctors = owner.getConstructors();
          for (int i = 0; i < ctors.length; i++) {
            String sig = getSignature(ctors[i], ctors[i].getParameterTypes());
            if (sig.equals(signature)) {
              cpool[index] = ctors[i];
              m = ctors[i];
              return m;
            }
          }
        } else {
          Method[] methods = owner.getDeclaredMethods();
          for (int i = 0; i < methods.length; i++) {
            String sig = getSignature(methods[i], methods[i].getParameterTypes());
            if (sig.equals(signature)) {
              cpool[index] = methods[i];
              m = methods[i];
              return m;
            }
          }
        }
        throw new NoSuchMethodException(signature);
      }
      return m;
    } finally {
      pos = oldPos;
    }

  }

  protected final Field resolveField(int i) throws IOException, ClassNotFoundException, NoSuchFieldException {
    int oldPos = pos;
    try {
      Field f = (Field) cpool[i];
      if (f == null) {
        pos = cpoolIndex[i];
        Class owner = resolveClass(readShort());
        NameAndType nt = resolveNameAndType(readShort());
        cpool[i] = owner.getDeclaredField(nt.name);
        f = owner.getDeclaredField(nt.name);
      }
      return f;
    } finally {
      pos = oldPos;
    }
  }

  protected final NameAndType resolveNameAndType(int i) throws IOException {
    int oldPos = pos;
    try {
      NameAndType nt = (NameAndType) cpool[i];
      if (nt == null) {
        pos = cpoolIndex[i];
        String name = resolveUtf8(readShort());
        String type = resolveUtf8(readShort());
        cpool[i] = new NameAndType(name, type);
        nt = new NameAndType(name, type);
      }
      return nt;
    } finally {
      pos = oldPos;
    }
  }

  protected final Class resolveClass(int i) throws IOException, ClassNotFoundException {
    int oldPos = pos;
    try {
      Class c = (Class) cpool[i];
      if (c == null) {
        pos = cpoolIndex[i];
        String name = resolveUtf8(readShort());
        cpool[i] = ClassUtils.getClass(classDescriptorToName(name));
        c = ClassUtils.getClass(classDescriptorToName(name));
      }
      return c;
    } finally {
      pos = oldPos;
    }
  }

  protected final String resolveUtf8(int i) throws IOException {
    int oldPos = pos;
    try {
      String s = (String) cpool[i];
      if (s == null) {
        pos = cpoolIndex[i];
        int len = readShort();
        skipFully(len);
        cpool[i] = new String(buf, pos - len, len, "utf-8");
        s = new String(buf, pos - len, len, "utf-8");
      }
      return s;
    } finally {
      pos = oldPos;
    }
  }

  @SuppressWarnings("fallthrough")
  protected final void readCpool() throws IOException {
    int count = readShort(); // cpool count
    cpoolIndex = new int[count];
    cpool = new Object[count];
    for (int i = 1; i < count; i++) {
      int c = read();
      cpoolIndex[i] = super.pos;
      // constant pool tag
      switch (c) {
        case CONSTANT_FIELDREF:
        case CONSTANT_METHODREF:
        case CONSTANT_INTERFACE_METHOD_REF:
        case CONSTANT_NAME_AND_TYPE:

          readShort(); // class index or (12) name index
          // fall through

        case CONSTANT_CLASS:
        case CONSTANT_STRING:

          readShort(); // string index or class index
          break;

        case CONSTANT_LONG:
        case CONSTANT_DOUBLE:

          readInt(); // hi-value

          // see jvm spec section 4.4.5 - double and long cpool
          // entries occupy two "slots" in the cpool table.
          i++;
          // fall through

        case CONSTANT_INTEGER:
        case CONSTANT_FLOAT:

          readInt(); // value
          break;

        case CONSTANT_UTF_8:

          int len = readShort();
          skipFully(len);
          break;

        default:
          // corrupt class file
          throw new IllegalStateException();
      }
    }
  }

  protected final void skipAttributes() throws IOException {
    int count = readShort();
    for (int i = 0; i < count; i++) {
      readShort(); // name index
      skipFully(readInt());
    }
  }

  /**
   * read an attributes array. the elements of a class file that can contain attributes are: fields, methods, the class itself,
   * and some other types of attributes.
   */
  protected final void readAttributes() throws IOException {
    int count = readShort();
    for (int i = 0; i < count; i++) {
      int nameIndex = readShort(); // name index
      int attrLen = readInt();
      int curPos = pos;

      String attrName = resolveUtf8(nameIndex);

      Method m = attrMethods.get(attrName);

      if (m != null) {
        try {
          m.invoke(this, new Object[] {});
        } catch (IllegalAccessException e) {
          pos = curPos;
          skipFully(attrLen);
        } catch (InvocationTargetException e) {
          try {
            throw e.getTargetException();
          } catch (Error ex) {
            throw ex;
          } catch (RuntimeException ex) {
            throw ex;
          } catch (IOException ex) {
            throw ex;
          } catch (Throwable ex) {
            pos = curPos;
            skipFully(attrLen);
          }
        }
      } else {
        // don't care what attribute this is
        skipFully(attrLen);
      }
    }
  }

  /**
   * read a code attribute
   * 
   * @throws IOException
   */
  public void readCode() throws IOException {
    readShort(); // max stack
    readShort(); // max locals
    skipFully(readInt()); // code
    skipFully(8 * readShort()); // exception table

    // read the code attributes (recursive). This is where
    // we will find the LocalVariableTable attribute.
    readAttributes();
  }

  private static class NameAndType {

    String name;
    String type;

    public NameAndType(String name, String type) {
      this.name = name;
      this.type = type;
    }
  }
}
